package tjs.study.notes.dotest.zookper.locker.common;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.LongAdder;

/**
 * Created by Nicky.Tang on 2021/11/10 3:01 下午
 *
 * @since 02.12.10
 */
public class TestLockSegmentCommon {
    private static final Logger logger = LoggerFactory.getLogger(TestLockSegmentCommon.class);
    public static final LongAdder hashBuyNum = new LongAdder();
    public static final LongAdder hashFieldNum = new LongAdder();

    private JedisPool pool;
    private RLockFactory rLockFactory;

    public TestLockSegmentCommon(RLockFactory rLockFactory) {
        this.rLockFactory = rLockFactory;

        JedisPoolConfig config = new JedisPoolConfig();
        //设置最大连接数
        config.setMaxTotal(-1);
        config.setMaxIdle(100);
        config.setMinIdle(800);
        JedisPool pool = new JedisPool(config, "127.0.0.1", 6379);
        this.pool = pool;
    }

    public TestLockSegmentCommon(JedisPool pool, RLockFactory rLockFactory) {
        this.pool = pool;
        this.rLockFactory = rLockFactory;
    }

    /**
     * @param goodsId      商品id+本次秒杀活动id
     * @param useNum       用户请求数
     * @param inventoryNum 库存总数
     * @param segmentNum   分成多少段
     * @throws InterruptedException
     */
    public void doTestSegment(String goodsId, int useNum, int inventoryNum, int segmentNum) throws Exception {
        // 将库存总数inventoryNum，拆分成segmentNum段
        initSegment(goodsId, inventoryNum, segmentNum);

        CountDownLatch countDownLatchMake = new CountDownLatch(useNum);
        CountDownLatch countDownLatchFinsh = new CountDownLatch(useNum);

        for (int i = 0; i < useNum; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    countDownLatchMake.await();// 等待countDownLatch为0
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // 开始抢
                String userId = "user_" + finalI;
                logger.info("用户：{}，开抢。。。 ", userId);
                try {
                    doBuyGoods(userId, goodsId);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                countDownLatchFinsh.countDown();
            }).start();
            countDownLatchMake.countDown();//减1
        }

        countDownLatchFinsh.await();
        logger.info("总库存数：{}, 实际卖出：{}，分成几段：{}", inventoryNum, hashBuyNum, segmentNum);
        logger.info("总人数：{}, 失败人数：{}", useNum, hashFieldNum);

    }

    private void doBuyGoods(String userId, String goodsId) throws Exception {
        // 1. 获取当前请求的分段锁的key
        try (Jedis jedisl = pool.getResource()) {
            String curSegmentIndex = getLocakSegmentIndex(jedisl, userId, goodsId);
            if (curSegmentIndex == null) {
                hashFieldNum.increment();
                throw new RuntimeException(userId + " 秒杀已售罄。。。");
            }
            String curSegmentKey = goodsId + "_index=" + curSegmentIndex;

            // 2.1 判断是否已参与
            Boolean hasGet = jedisl.sismember("suc_" + goodsId, userId);
            if (hasGet) {
                throw new RuntimeException(userId + " 您已参与1。。。");
            }

            // 2.2 尝试加锁
            try (RLock rLock = rLockFactory.getLock(curSegmentKey)) {
                if (rLock.lock()) {
                    String logKey = userId + ":" + rLock.getLockPath();

                    // 3. 判断库存是否充足
                    String curSegmentSurplusStr = jedisl.hget(goodsId, curSegmentIndex);
                    Long curSegmentSurplus = null;
                    if (curSegmentSurplusStr == null || (curSegmentSurplus = Long.valueOf(curSegmentSurplusStr)) <= 0) {
                        // 可能getLock时该段还有库存，等了很久获取到锁后就没库存了
                        logger.info("{} 成功持有锁，但是库存不够了，自动重试。。。", logKey);
                        rLock.unlock();
                        doBuyGoods(userId, goodsId);
                    }

                    // 4. 扣减库存 todo 如果这一步后面的代码异常了，回滚如何加回
                    Long success = (Long) jedisl.eval("if (redis.call('SISMEMBER', KEYS[1], ARGV[1]) == 0) then " +
                            "redis.call('sadd', KEYS[1], ARGV[1]); " +
                            "redis.call('HINCRBY', KEYS[2], ARGV[2], -1); " +
                            "return 1; " +
                            "end; " +
                            "return 0;", 2, "suc_" + goodsId, goodsId, userId, curSegmentIndex);
                    if(!new Long(1).equals(success)){
                        rLock.unlock();
                        throw new RuntimeException(userId + " 您已参与2。。。");
                    }
                    hashBuyNum.increment();


                    // 5. 加锁成功，执行业务代码（异步生成订单）
                    logger.info("{} 成功持有锁，开始执行业务。。。", logKey);
                    doSomeOtherThings();

                    // 6. 扣减后，若为0，则移除当前段
                    if (curSegmentSurplus <= 0) {
                        logger.info("第 {} 段,本次扣减后库存为 {},开始移除。。。{}", curSegmentKey, curSegmentSurplus, logKey);
                        // 6.1 扣减后，若库存小于0（可能分布式锁异常了，导致多个请求持有了锁）
                        if (curSegmentSurplus < 0) {
                            throw new RuntimeException(userId + " 秒杀已售罄,分布式锁异常。。。");
                        }
                        //jedisl.hdel(goodsId, curSegmentKey);
                    }

                    logger.info("{} 开始释放锁，已结束执行业务。。。", logKey);
                    // rLock.unlock();//交给编译后的AutoCloseable
                } else {
                    doBuyGoods(userId, goodsId);
                }
            }
        }
    }

    private void doSomeOtherThings() {
        try {
            Thread.currentThread().sleep(new Random().nextInt(10) * 300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void initSegment(String goodsId, int inventoryNum, int segmentNum) {
        // 将库存总数inventoryNum，拆分成segmentNum段
        Map<String, String> segmentMap = new HashMap<>();
        int oneNum = inventoryNum / segmentNum;
        int lastNum = segmentNum - 1;
        for (int i = 0; i < segmentNum; i++) {
            if (i == lastNum) {
                int surplusNum = inventoryNum - oneNum * (i);
                segmentMap.put(String.valueOf(i), String.valueOf(surplusNum));
            } else {
                segmentMap.put(String.valueOf(i), String.valueOf(oneNum));
            }
        }
        Jedis jedisl = pool.getResource();
        jedisl.del(goodsId);
        jedisl.hset(goodsId, segmentMap);
    }

    /**
     * 获取当前用户应该加锁哪一段
     *
     * @return
     */
    private String getLocakSegmentIndex(Jedis jedisl, String userId, String goodsId) {
        String locakSegmentKey = goodsId;
        //Long segmentAllNum = jedisl.hlen(goodsId);//不可以直接获取长度，可能扣减库存后，异常了导致未及时删除该key
        // 1. 获取redis中【有效】分段锁的数量（如果段数很多，考虑用lua脚本获取）
        Map<String, String> allNum = jedisl.hgetAll(goodsId);
        List<String> reallyNums = new ArrayList<>();//有效】分段锁数量
        for (Map.Entry<String, String> entry : allNum.entrySet()) {
            if (entry.getValue() != null && Integer.valueOf(entry.getValue()) > 0) {
                reallyNums.add(entry.getKey());
            }
        }
        if (reallyNums.isEmpty()) {
            return null;// 已售罄
        }
        // 2. 与用户id求膜
        Integer curSegmentIndex = Math.abs(userId.hashCode()) % reallyNums.size();
        return reallyNums.get(curSegmentIndex).toString();
    }

}
